Skip to content

bch: forward txs on the send path via bch::new_tx_hashes accessor (#905) - #913

Merged
frstrtr merged 1 commit into
masterfrom
bch/tx-forward-probe-fix
Jul 27, 2026
Merged

bch: forward txs on the send path via bch::new_tx_hashes accessor (#905)#913
frstrtr merged 1 commit into
masterfrom
bch/tx-forward-probe-fix

Conversation

@frstrtr

@frstrtr frstrtr commented Jul 27, 2026

Copy link
Copy Markdown
Owner

What

send_shares() (src/impl/bch/node.cpp) decided which peer-referenced txs to forward by probing requires { obj->m_new_transaction_hashes; } directly on the share object. That flat spelling is false for every BCH share variant:

variant version new-tx list
bch::Share v17 m_tx_info.m_new_transaction_hashes (nested)
bch::NewShare v33 m_tx_info.m_new_transaction_hashes (nested)
bch::SegwitMiningShare v34 none (SegWit-less, no m_tx_info)
bch::PaddingBugfixShare v35 none
bch::MergedMiningShare v36 none

So the entire needed_txs / remember_tx forwarding block was unreachable — BCH forwarded no tx bytes, and a relayed v17/v33 share referencing a tx the peer lacked was exactly the canonical "referenced unknown transaction" disconnect (#905). The receive side (protocol_actual.cpp, protocol_legacy.cpp, node.hpp) already spells m_tx_info.m_new_transaction_hashes correctly, which is why only the send side was dead.

Fix

New src/impl/bch/share_tx_refs.hppbch::new_tx_hashes(obj) returns the nested list for v17/v33 and nullptr for v34+ (nothing to forward). Direct port of ltc::new_tx_hashes (#873, ced0d0b); BCH variant topology is identical. send_shares() now routes through it. A new variant is either handled in one of the two known spellings or reported as "no list" — never silently skipped on a member-name mismatch again.

seam-2 (post-broadcast lock discipline) is a STRUCTURAL NO-OP for BCH

LTC #873 carried a second seam: send_shares() was made to return the hashes it actually wrote, and broadcast_share was reworked so the tracker lock is released before the mining-notify / broadcast fan-out (its ingest path remember_tx inserts into m_known_txs under a different discipline, so reading that map mid-broadcast under the held lock was the hazard #873 closed).

BCH does not need that change, and it is not ported here — established by inspection of the BCH broadcast path, not by analogy:

  • BCH's submit/broadcast entrypoint already calls lk.unlock() on the tracker lock before the broadcast + mining_submit fan-out (pool_entrypoint.hpp:561); the broadcast runs on the io thread with no share-tracker lock held. The held-lock-across-broadcast ordering that ltc/doge: gate share broadcast on tx completeness, mark only what was sent #873 had to fix never existed on BCH.
  • BCH send_shares() is void and needs no return value: the LTC return-hashes plumbing existed only to let the reworked broadcast_share mark what was actually sent under the new lock ordering. With no ordering to change, there is nothing to report back.

i.e. the invariant #873's seam-2 established for LTC is one BCH already holds structurally. This PR is the seam-1 port only; there is no BCH seam-2 to land. (DGB shares the same seam-1 gap on its own lane and must check its own seam-2 separately; LTC is done via #873.)

Compatibility

Share wire-format unchanged — this is relay plumbing over the existing nested list. p2pool-merged-v36 surface: none.

Test

bch_share_tx_refs_kat_test (new) pins:

  • compile-time topology — flat spelling absent on every variant (== the old probe was dead), nested present exactly on v17/v33, accessor null-branch for v34+;
  • runtime delivery — v17/v33 list forwarded by reference, v34+ forward nothing.

Registered in src/impl/bch/test/CMakeLists.txt (BCH_ABLA_TESTS) and both COIN_BCH build.yml --target lists (main + AsAN legs). Local: bch_ suite builds clean, c2pool-bch links, KAT (ctest #326) PASS.

Merge held for integrator (push-approval card).

send_shares() probed `requires { obj->m_new_transaction_hashes; }` directly on
the share object to decide which referenced txs to push to a peer. That flat
spelling is FALSE for every BCH share variant -- v17/v33 nest the list inside
m_tx_info, v34/v35/v36 (SegWit-less) carry no list at all -- so the needed_txs
forwarding block was unreachable and BCH forwarded no tx bytes. A relayed
v17/v33 share referencing a tx the peer lacked was the canonical "referenced
unknown transaction" disconnect. The receive side (protocol_actual/legacy,
node.hpp) already spells m_tx_info.m_new_transaction_hashes correctly, which is
why only the send side was dead.

Route the probe through a single accessor, mirroring ltc::new_tx_hashes (#873):
new src/impl/bch/share_tx_refs.hpp returns the nested list for v17/v33 and
nullptr for v34+ (nothing to forward) -- a new variant is either handled in one
of the two known spellings or reported as "no list", never silently skipped on a
member-name mismatch.

Guard: share_tx_refs_kat_test pins the topology (flat spelling absent on every
variant, nested present exactly on v17/v33, accessor null-branch for v34+) and
the runtime delivery (v17/v33 list forwarded by reference, v34+ forward nothing).
Registered in the BCH test CMake list and both COIN_BCH build.yml --target
lists (main + AsAN legs). Share wire-format unchanged; p2pool-merged-v36
surface: none.
@frstrtr
frstrtr merged commit eb8616b into master Jul 27, 2026
40 checks passed
frstrtr added a commit that referenced this pull request Jul 27, 2026
…coin new_tx_hashes SSOT

The old flat m_new_transaction_hashes probe was FALSE for every share
variant (v17/v33 nest the refs in m_tx_info; v34+ carry none), so the F3
gate collected empty refs and treated every share as vacuously backable
-- it never withheld an incomplete share on any version. Route each
coin through its SSOT accessor from #913/#914 (btc/bch::new_tx_hashes,
dgb::append_share_tx_refs) so the gate sees the real refs.

Adds BtcBroadcastGate.RealShareTxInfoRefsWithheld driving a REAL v17/v33
share (not a top-level mock); mutation-verify confirms it goes red when
the probe is reverted to the dead flat form.
frstrtr added a commit that referenced this pull request Jul 29, 2026
… was sent (#880)

* btc/dgb/bch: gate share broadcast on tx completeness + mark only what was sent

Two reward-critical share-loss fixes, ported to BTC, DGB and BCH from the
already-correct DASH implementation (src/impl/dash/node.cpp).

F3 -- tx-completeness broadcast gate. send_shares() collected the referenced
new-tx bytes with `if (it != m_known_txs.end()) full_txs.emplace_back(...)`
and no else branch: a tx we do not hold was silently omitted and the share was
written to the peer anyway. Canonical p2pool then drops the connection on
"referenced unknown transaction" (p2p.py), which isolates us from the
sharechain and orphans our shares. Shares pulled via sharereply arrive without
their tx bytes, so this is reachable in normal operation. send_shares now
filters the outgoing batch through partition_backable() (gate on the BYTES in
m_known_txs, not on the peer's possibly-stale have_tx advert) and the
now-unreachable omission path increments a counter that logs a warning.

F2 -- mark only what was sent. broadcast_share() inserted each walked hash
into m_shared_share_hashes INSIDE the chain-walk loop, before send_shares ran.
send_shares returned void and has abandon paths (tracker try_to_lock miss,
empty batch, and now the F3 gate), and zero connected peers abandons the batch
too. A marked-but-unsent share is lost permanently: the next walk breaks on
the first marked hash, so nothing ever re-pushes it -- silent PPLNS-credit
loss with no retry path. send_shares now returns the hashes it actually wrote
and broadcast_share marks only those, after the peer loop, via
broadcast_and_mark(). m_last_broadcast_to likewise records what was written
rather than what was offered, so a withheld share is never blamed for a peer
disconnect (which would mark it rejected and block all future re-broadcast).

Consensus surface: NONE. Both changes decide only WHETHER a share goes on the
wire. Share bytes, minting, PPLNS weighting and payout are untouched, and a
withheld share stays in the chain and is re-offered once its txs arrive.

Primitives follow the per-coin header pattern established for BTC in #868:
src/impl/{btc,dgb,bch}/known_txs_retention.hpp. The LTC lane is concurrently
hoisting all_txs_backable into src/core/; these four per-coin headers
(including dash) should be collapsed onto that hoist once both land.

Tests ride existing allowlisted CI targets -- no new add_executable, no
build.yml change:
  btc  -> btc_share_test  (broadcast_gate_test.cpp)
  dgb  -> dgb_share_test  (broadcast_gate_test.cpp, known_txs_retention_test.cpp)
  bch  -> bch_embedded_block_broadcast_test (broadcast_gate_test.cpp; the bch
          test tree has no GTest harness, so the TU exposes a checks function
          the host main() calls)

Mutation-verified red: deleting the gate reddens 2 btc / 4 bch assertions;
restoring mark-before-send reddens 4 btc / 5 bch assertions.

* dgb: rescope to the reachable readvertise path; label broadcast_share pre-wiring

A caller-side reachability trace shows DGB is not symmetric with btc/bch:
main_dgb.cpp binds no create/mint-share fn, so dgb::NodeImpl::broadcast_share
and notify_local_share have zero callers repo-wide -- DGB never mints a local
share today (#884). DGB's live path into send_shares is readvertise_best_share()
(ROOT-2 re-advert on best-change plus a 10s one-shot timer), which re-pushes
PEER-RECEIVED shares and deliberately bypasses the de-dup set.

Verified independently: set_mint_share_fn / set_create_share_fn / broadcast_share
/ notify_local_share have zero occurrences in main_dgb.cpp; the only
set_mint_share_fn binding in the tree is main_dash.cpp:1854 plus dgb's own
work_source_test. readvertise_best_share is called from node.cpp:1767 and 1774.
BTC (main_btc.cpp:1336 after lk.unlock() at :1323) and BCH
(pool_entrypoint.hpp:564 after lk.unlock() at :562) are both live and unchanged.

Changes:

- dgb: extract the readvertise recording rule into readvertise_and_record() and
  wire readvertise_best_share() through it. This is DGB's live reward-critical
  marking discipline: m_last_broadcast_to is what a peer disconnect within 10s
  converts into m_rejected_share_hashes, and the readvertise walk `continue`s
  past rejected hashes permanently. Recording the OFFERED batch would let a
  share the F3 gate merely withheld be blamed for someone else's drop and
  excluded from every future re-advert -- a propagation loss for the whole
  sharechain, since DGB re-advertises peer-received shares.

- dgb: rescope the KATs onto that live path. The gate cases (partition_backable)
  and the new DgbReadvertiseRecording cases drive code production executes; the
  broadcast_and_mark cases are renamed
  DgbBroadcastMarkingPrewired_NotReachedToday so no reader mistakes them for
  live protection. Removes the broadcast_share de-dup-walk KAT, which modelled
  an unreachable function -- the shape of the LTC #873 miss.

- dgb: label NodeImpl::broadcast_share in source as pre-wiring for #884. The F2
  fix stays so the seam is correct when the mint path is bound and so the three
  coins remain symmetric, but it protects nothing at runtime today.

- btc/bch: document the ROOT-2 interaction at readvertise_best(). Marking is now
  a strict subset of what the pre-fix code marked, so the walk breaks later or
  in the same place, never earlier: the re-advert re-pushes a superset of what
  it used to. Monotonically better, strictly better for head shares minted while
  no peer was connected. It does not fully close ROOT-2 here -- shares already
  accepted by another peer stay marked and the walk still breaks, as before.
  That needs the ltc/dgb-style de-dup-bypass readvertise; pre-existing, out of
  scope. No behaviour change in this commit for btc/bch.

Mutation-verified red on the DGB live path: recording the offered batch instead
of what was written reddens WithheldShareIsNeverBlamedForAPeerDrop; dropping the
empty-send skip reddens PeerThatReceivedNothingIsNotRecorded; deleting the gate
reddens 2 DgbBroadcastGate cases.

* btc/bch/dgb: route F3 send-path tx-completeness gate through the per-coin new_tx_hashes SSOT

The old flat m_new_transaction_hashes probe was FALSE for every share
variant (v17/v33 nest the refs in m_tx_info; v34+ carry none), so the F3
gate collected empty refs and treated every share as vacuously backable
-- it never withheld an incomplete share on any version. Route each
coin through its SSOT accessor from #913/#914 (btc/bch::new_tx_hashes,
dgb::append_share_tx_refs) so the gate sees the real refs.

Adds BtcBroadcastGate.RealShareTxInfoRefsWithheld driving a REAL v17/v33
share (not a top-level mock); mutation-verify confirms it goes red when
the probe is reverted to the dead flat form.

* btc: mirror ltc broadcast lock-discipline trace; pin send-path reachability

Instrument btc::NodeImpl::broadcast_share with the deferred/acquired/
reached-send counters and IsNull/contains guards that ltc already
carries, and add broadcast_lock_discipline_test.cpp driving a REAL
btc::NodeImpl against a populated sharechain. Proves the F3 send path
(tx-completeness gate + send_shares + mark-after-send) is reachable, not
dead code behind an always-held lock: reached_send is 0 when the caller
holds the tracker mutex EXCLUSIVELY on the calling thread and 1 when it
does not -- the shape main_btc create_share_fn guarantees by dropping
the exclusive lock (lk.unlock) before calling broadcast_share. Rides the
allowlisted btc_share_test target (no new add_executable, #769).

---------

Co-authored-by: frstrtr <frstrtr@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant